import re

from threading import Thread
from typing import Dict, Callable, List, Union, Tuple, Any, Pattern

import requests
import urllib3

# disable warning for SSL
urllib3.disable_warnings()


class FileNode(object):
    FILE_TYPE_UNKNOWN = 'N/A'
    FILE_TYPE_DIR = 'DIR'
    FILE_TYPE_TXT = 'TXT'
    FILE_TYPE_VID = 'VID'

    _PATTERN_SW_LIKE = re.compile(r'P[\w+.mbn]')

    DOWNLOAD_BUFFER_SIZE = 512 * 1024

    def __init__(self, parent, host: str, path: str, date: str, file_type: str, auth: Any,
                 pattern_line: Pattern, pattern_info: Pattern):
        self.parent: FileNode = parent
        self.host = FileNode._format(host)
        self.path = FileNode._format(path)
        self.date = date
        self.auth = auth
        self._PATTERN_FILE_LINE = pattern_line
        self._PATTERN_FILE_INFO = pattern_info
        self.file_type = file_type if file_type != '' else FileNode.FILE_TYPE_UNKNOWN
        self.children: Union[None, Dict[str, FileNode]] = None
        self.sw_like = False

        parts = self.path.split('/')
        self.name = parts[len(parts) - 1]

    def __str__(self):
        return '[%s][%s] %s' % (self.file_type, self.date, self.path)

    def display_name(self) -> str:
        return '[%s][%s] %s' % (self.file_type, self.date, self.name)

    def is_dir(self) -> bool:
        return self.file_type == FileNode.FILE_TYPE_DIR

    def url(self) -> str:
        return '%s/%s' % (self.host, self.path)

    @staticmethod
    def _format(path: str):
        return path.strip('[ /]')

    def _load_dir(self, force=False):
        if not self.is_dir():
            print('current not is not dir')
            return

        if self.children is None or force:
            # download file
            content = self.download()

            # parser dir
            self._parser_dir(content)

    def download(self, on_progress: Callable[[int, int, bytes], None] = None) -> Union[None, bytes]:
        if on_progress:
            # async mode
            thread = Thread(target=lambda: self._download_internal(on_progress))
            thread.start()
            return None
        else:
            # sync mode
            data_list: List[bytes] = []

            # download callback
            def on_download_progress(cur: int, total: int, data: bytes):
                nonlocal data_list
                data_list.append(data)

            # download file
            self._download_internal(on_download_progress)
            content = b''.join(data_list)

            return content

    def _parser_dir(self, content: bytes):
        # do load
        self.sw_like = False
        self.children = dict()  # drop old children
        found = re.findall(self._PATTERN_FILE_LINE, content.decode())
        for f in found:
            m = re.match(self._PATTERN_FILE_INFO, f)
            if m:
                filename = m.group(2).strip('[ /]')
                date = m.group(3).strip()
                str_type = m.group(1).strip()
                if self._PATTERN_SW_LIKE.match(filename) is not None:
                    self.sw_like = True
                node = FileNode(self, self.host, '%s/%s' % (self.path, filename), date, str_type, self.auth,
                                self._PATTERN_FILE_LINE, self._PATTERN_FILE_INFO)
                self.children[filename] = node

    def download_size(self) -> int:
        with requests.get(self.url(), verify=False, auth=self.auth, stream=True) as response:
            if 'content-length' in response.headers:
                file_size = response.headers['content-length'].strip()
                if file_size == '':
                    return -1
                else:
                    return int(file_size)
            else:
                return -1  # size unknown

    def _download_internal(self, on_progress: Callable[[int, int, bytes], None]):
        # send request
        with requests.get(self.url(), verify=False, auth=self.auth, stream=True) as response:
            if 'content-length' in response.headers:
                file_size = response.headers['content-length'].strip()
                if file_size == '':
                    file_size = -1
                else:
                    file_size = int(file_size)
            else:
                file_size = -1  # size unknown

            # update progress for start
            size_downloaded: int = 0
            on_progress(size_downloaded, file_size, b'')

            # do download
            for chunk in response.iter_content(self.DOWNLOAD_BUFFER_SIZE):
                size_downloaded += len(chunk)
                on_progress(size_downloaded, file_size, chunk)

            # update progress for finished
            on_progress(size_downloaded, file_size, b'')

    def child(self, filename: Union[str, int]):
        if not self.is_dir():
            print('current node is not dir')
            return None

        if isinstance(filename, str):
            # reload before get a child
            self._load_dir()

            filename = FileNode._format(filename)
            if filename == '' or filename == '.':
                return self

            if filename == '..':
                return self.parent

            if filename in self.children:
                return self.children[filename]
            else:
                return None
        elif isinstance(filename, int):
            self._load_dir()
            values = list(self.children.values())
            if 0 <= filename < len(values):
                return values[filename]
            else:
                return None
        else:
            return None

    def list(self) -> Tuple[List, List[str]]:
        self._load_dir()
        return list(self.children.values()), list(self.children.keys())

    def ls(self) -> Tuple[List[str], List[str]]:
        self._load_dir()
        paths = []
        names = []

        for node in self.children.values():
            paths.append(node.pwd())
            names.append(node.name)

        return paths, names,

    def pwd(self) -> str:
        return '/%s' % self.path

    def refresh(self, force=True):
        self._load_dir(force)

    def is_sw_like(self) -> bool:
        return self.sw_like
